/*
 * Famon (http://code.google.com/p/famon/)
 * Copyright (c) 2010 Carl Crous.
 *
 * This file is part of Famon.
 *
 * Famon is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Famon is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Famon.  If not, see <http://www.gnu.org/licenses/>.
 */

package za.org.foobar.famon.frontend.client.framework;

import com.google.gwt.core.client.JavaScriptException;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Event;
import org.gwtportlets.portlet.client.util.GenUtil;

import java.util.Collection;

/**
 * High performance grid.
 */
public class FastGrid extends Widget 
        implements SourcesTableEvents, SourcesMouseEvents {

    protected Object[] data;
    protected int rowCount;
    protected Renderer renderer;
    protected StringBuffer html;

    protected Element divElem;
    protected Element tableElem;
    protected Element bodyElem;

    protected String tableStyleName;
    protected String borderWidth;
    protected String cellPadding;
    protected String cellSpacing;
    protected String width;

    protected TableListenerCollection tableListeners;
    protected MouseListenerCollection mouseListeners;

    private Event lastEvent;

    public FastGrid() {
        setElement(divElem = DOM.createDiv());
        tableElem = DOM.createTable();
        DOM.appendChild(divElem, tableElem);
        sinkEvents(Event.ONCLICK | Event.MOUSEEVENTS);
        setStyleName("FastGrid");
        setCellPadding(4);
        setCellSpacing(0);
        setBorderWidth(1);
    }

    public FastGrid(Renderer renderer) {
        this();
        setRenderer(renderer);
    }

    public Renderer getRenderer() {
        return renderer;
    }

    public void setRenderer(Renderer renderer) {
        this.renderer = renderer;
    }

    public void setStyleName(String style) {
        tableStyleName = style;
    }

    public String getStyleName() {
        return tableStyleName;
    }

    public void setBorderWidth(int width) {
        borderWidth = Integer.toString(width);
    }

    public void setCellPadding(int padding) {
        cellPadding = Integer.toString(padding);
    }

    public void setCellSpacing(int spacing) {
        cellSpacing = Integer.toString(spacing);
    }

    public String getWidth() {
        return width;
    }

    public void setWidth(String width) {
        this.width = width;
    }

    /**
     * Please note if your grid has a header row 1 is the fisrt row of data.
     * ie. you have added null at some point, typically this would be
     * done when you create the grid.
     */
    public Object getData(int row) {
        return data[row];
    }

    public void setData(int row, Object value) {
        data[row] = value;
    }

    /**
     * Add data to the grid.
     */
    public void addAllData(Object[] a) {
        if (a != null) {
            for (Object o : a) {
                addData(o);
            }
        }
    }

    /**
     * Add data to the grid.
     */
    public void addAllData(Collection c) {
        for (Object o : c) {
            addData(o);
        }
    }

    /**
     * Add data to the grid.
     */
    public void addData(Object rowData) {
        if (data == null || rowCount == data.length) {
            if (data == null) {
                data = new Object[16];
            } else {
                Object[] a = new Object[rowCount * 2];
                for (int i = 0; i < rowCount; i++) {
                    a[i] = data[i];
                }
                data = a;
            }
        }
        data[rowCount++] = rowData;
    }

    /**
     * Remove data from the grid.
     */
    public void removeData(int row) {
        for (int i = row; i < rowCount - 1; i++) {
            data[i] = data[i + 1];
        }
        --rowCount;
    }

    /**
     * Remove all data from the grid.
     */
    public void clearData() {
        data = null;
        rowCount = 0;
    }

    public int getRowCount() {
        return rowCount;
    }

    /**
     * Repaint our table. This rebuilds the HTML for the table from scratch
     * and sets the innerHTML for our DIV.
     */
    public void repaint() {
        html = new StringBuffer();
        startTable(html);
        for (int i = 0; i < rowCount; i++) {
            renderTR(html, i);
        }
        renderAfterLastRow(html);
        html.append("</tbody></table>");
        DOM.setInnerHTML(divElem, html.toString());
        tableElem = DOM.getFirstChild(divElem);
        bodyElem = DOM.getFirstChild(tableElem);
    }

    protected void startTable(StringBuffer buf) {
        buf.append("<table border=\"");
        buf.append(borderWidth);
        buf.append("\" cellspacing=\"");
        buf.append(cellSpacing);
        buf.append("\" cellpadding=\"");
        buf.append(cellPadding);
        buf.append("\" class=\"");
        buf.append(tableStyleName);
        if (width != null) {
            buf.append("\" width=\"");
            buf.append(width);
        }
        buf.append("\"><tbody>");
    }

    protected void renderTR(StringBuffer buf, int row) {
        renderer.renderTR(this, buf, row, data[row]);
    }
    
    protected void renderAfterLastRow(StringBuffer buf) {
    }

    public void onBrowserEvent(Event event) {
//        URLActionManager.get().onBrowserEvent(this, event);
        lastEvent = event;
        switch (DOM.eventGetType(event)) {
            case Event.ONCLICK:
                Element td = getEventTargetCell(event);
                if (td == null) {
                    return;
                }
                Element tr = DOM.getParent(td);
                Element body = DOM.getParent(tr);
                int row = DOM.getChildIndex(body, tr);
                int column = DOM.getChildIndex(tr, td);
                fireCellClicked(event, row, column);
                break;
            case Event.ONMOUSEDOWN:
            case Event.ONMOUSEUP:
            case Event.ONMOUSEMOVE:
            case Event.ONMOUSEOVER:
            case Event.ONMOUSEOUT:
                if (mouseListeners != null) {
                    mouseListeners.fireMouseEvent(this, event);
                }
                break;
        }
    }

    /**
     * Get the last event object we have received.
     */
    public Event getLastEvent() {
        return lastEvent;
    }

    protected void fireCellClicked(Event event, int row, int column) {
        String token = GenUtil.getTargetHistoryToken(event);
        if (token == null && tableListeners != null) {
            tableListeners.fireCellClicked(this, row, column);
        }
    }

    /**
     * Determines the TD associated with the specified event or null if none.
     * Its parent (<code>DOM.getParent(td)</code>) is a TR.
     */
    public Element getEventTargetCell(Event event) {
        Element td;
        try {
            td = DOM.eventGetTarget(event);
        } catch (JavaScriptException e) {
            // sometimes getting the target throws an exception for some reason
            return null;
        }
        for (; td != null; td = DOM.getParent(td)) {
            // If it's a TD, it might be the one we're looking for.
            if (isTD(td)) {
                // Make sure it's directly a part of this table before returning it.
                Element tr = DOM.getParent(td);
                Element body = DOM.getParent(tr);
                if (DOM.compare(body, bodyElem)) {
                    return td;
                }
            }
            // If we run into this table's body, we're out of options.
            if (DOM.compare(td, bodyElem)) {
                return null;
            }
        }
        return null;
    }

    /**
     * Is element a td?
     */
    private static boolean isTD(Element element) {
        return DOM.getAttribute(element, "tagName").equalsIgnoreCase("td");
    }

    /**
     * Get the row for the element (a td or tr from this table) or -1 if
     * none.
     */
    public int getElementRow(Element element) {
        if (isTD(element)) {
            element = DOM.getParent(element);
        }
        return DOM.getChildIndex(bodyElem, element);
    }

    /**
     * Get the column for the td or -1 if none.
     */
    public int getElementCol(Element td) {
        return DOM.getChildIndex(DOM.getParent(td), td);
    }

    /**
     * Get the TR element at row.
     */
    public Element getTR(int row) {
        return DOM.getChild(bodyElem, row);
    }

    /**
     * Get the TD element at row, col.
     */
    public Element getTD(int row, int col) {
        return DOM.getChild(getTR(row), col);
    }

    /**
     * Get the inner text of the TD at row, col.
     */
    public String getText(int row, int col) {
        return DOM.getInnerText(getTD(row, col));
    }

    public void addTableListener(TableListener listener) {
        if (tableListeners == null) {
            tableListeners = new TableListenerCollection();
        }
        tableListeners.add(listener);
    }

    public void removeTableListener(TableListener listener) {
        if (tableListeners != null) {
            tableListeners.remove(listener);
        }
    }

    public void addMouseListener(MouseListener listener) {
        if (mouseListeners == null) {
            mouseListeners = new MouseListenerCollection();
        }
        mouseListeners.add(listener);
    }

    public void removeMouseListener(MouseListener listener) {
        if (mouseListeners != null) {
            mouseListeners.remove(listener);
        }
    }

    /**
     * Scroll so the row is visible. NOP if row is out of range or we are
     * not attached.
     */
    public void scrollIntoView(int row) {
        if (row > 0 && row < rowCount && isAttached()) {
            DOM.scrollIntoView(getTR(row));
        }
    }    

    /**
     * Provides HTML for a row on our grid.
     */
    public interface Renderer {

        /**
         * Append HTML for a TR for row to html.
         */
        public abstract void renderTR(FastGrid grid, StringBuffer html, int row,
                Object data);

    }

}
